Skip to main content

Resource list page

ResourceListPage and ResourceListPageContent are the standard building blocks for FHIR resource list views in a custom Beda EMR build. They load paginated search results, render a filterable table, and wire up header, row, and batch actions — including questionnaire modals and navigation links.

Sources:

Import:

import {
ResourceListPage,
navigationAction,
questionnaireAction,
customAction,
} from '@beda.software/emr/dist/uberComponents/ResourceListPage/index';

import { ResourceListPageContent } from '@beda.software/emr/dist/uberComponents/ResourceListPageContent/index';

When working inside the Beda EMR submodule, use the src/ paths instead:

import { ResourceListPage, navigationAction, questionnaireAction } from 'src/uberComponents/ResourceListPage';
import { ResourceListPageContent } from 'src/uberComponents/ResourceListPageContent';

ResourceListPage vs ResourceListPageContent

Both components share the same data-loading, filtering, sorting, pagination, and action logic via useResourceListPage. The difference is layout and where you use them.

ResourceListPageResourceListPageContent
ShellFull page with PageContainer — title, optional back button, header actionsContent block inside PageContainerContent (level 2)
Use caseTop-level routes registered in EMR (/patients, /encounters, …)Nested inside a detail page tab, patient chart section, or other existing layout
Extra propsheaderTitle, backButtonVisibleNone — inherits surrounding page chrome
Header questionnaire contextMerges defaultLaunchContext with getClinicalContext(undefined)Uses defaultLaunchContext only

Use ResourceListPage for standalone list routes. Use ResourceListPageContent when the list is part of a larger page — typically a tab on ResourceDetailPage.

What it provides

Both components compose:

  1. useResourceListPage — FHIR search with pagination (usePager), debounced filter params, row selection for batch actions, and a reload() callback.
  2. SearchBar — filters from getFilters(), synced with table column filters.
  3. Table — paginated data grid with optional row selection, sorting, and an auto-generated Actions column.
  4. Action helpersquestionnaireAction, navigationAction, and customAction for header, row, and batch buttons.

ResourceListPage additionally wraps everything in PageContainer with a page title and optional back navigation.

Route setup

Register a list page on a flat route in EMR:

<Route path="/encounters" element={<EncounterList />} />
<Route path="/patients/:id/*" element={<PatientDetails />} />

Pair list and detail routes so row navigationAction links resolve:

<Route path="/medications" element={<MedicationManagementList />} />
<Route path="/medications/:id/*" element={<MedicationManagementDetail />} />

See EMR component for route registration and Resource detail page for the detail side.

Props

Shared props come from ResourceListProps. Each row in the table is a RecordType<R>:

type RecordType<R extends Resource> = { resource: R; bundle: Bundle; children?: RecordType<R>[] };

bundle is the full FHIR search response for the current page. Use it to resolve _included resources (for example, Patient on an Encounter row).

ResourceListPage-only props

type ResourceListPageProps<R extends Resource> = ResourceListProps<R> & {
headerTitle: string;
backButtonVisible?: boolean;
maxWidth?: number | string;
getTableColumns: (manager: TableManager) => ColumnsType<RecordType<R>>;
};
PropRequiredDescription
headerTitleYesPage title shown in PageContainer.
backButtonVisibleNoWhen true, shows a back button that calls navigate(-1).
getTableColumnsYesColumn definitions (without the Actions column). Receives { reload } via TableManager.

Shared props (ResourceListProps)

PropRequiredDescription
resourceTypeYesPrimary FHIR resource type for rows (for example, "Encounter").
searchParamsNoDefault FHIR search parameters (_sort, _count, _include, …). Filter and sort values from the UI are merged on top.
extractPrimaryResourcesNoCustom function to pick primary rows from the search bundle. Default: all entries matching resourceType.
extractChildrenResourcesNoReturns child resources for tree rows (for example, organizations linked via partOf).
uniqueOrderSortSearchParamNoTie-breaker sort param appended to _sort for stable pagination. Default: -_lastUpdated. Set to null to disable.
getFiltersNoReturns SearchBarColumn definitions for the search bar and table filters.
getSortersNoReturns sorter definitions mapping column ids to FHIR _sort params.
getRecordActionsNoPer-row actions. When provided, an Actions column is appended automatically.
getHeaderActionsNoButtons in the page header (create, import, …).
getBatchActionsNoActions shown when rows are selected. Enables row checkboxes. Receives a Bundle of selected resources.
defaultLaunchContextNoParametersParameter[] merged into all questionnaire launch contexts.
getClinicalContextNoPer-scope clinical context for questionnaire actions. See Clinical context.
getReportColumnsNoExperimental. Summary report above the table from the current search bundle.
maxWidthNoMax content width.
tablePropsNoExtra props passed to the underlying Ant Design Table (excluding pagination, columns, data, etc.).

TableManager passed to getTableColumns and getRecordActions:

interface TableManager {
reload: () => void;
}

Call reload() after a mutation to refresh the current page.

Table columns

ResourceListPageContent — Ant Design columns

ResourceListPageContent expects standard Ant Design ColumnsType<RecordType<R>>. Define cell content in render:

getTableColumns={() => [
{
title: t`Lot number`,
key: 'lotNumber',
render: (_: unknown, record) => getMedicationLotNumber(record.resource) ?? '',
},
{
title: t`Expiration date`,
key: 'expirationDate',
render: (_: unknown, record) => getMedicationExpirationDate(record.resource) ?? '',
},
]}

The Actions column is still generated automatically when getRecordActions is provided.

Actions

Import action builders from ResourceListPage:

import { navigationAction, questionnaireAction, customAction } from 'src/uberComponents/ResourceListPage';

Questionnaire actions

Opens a modal with QuestionnaireResponseForm. On success, shows a notification and calls reload().

Define the form as a Questionnaire + Mapping pair on the FHIR server, then reference the questionnaire id here. See Questionnaire actions for the full authoring workflow.

questionnaireAction(<Trans>Edit</Trans>, 'healthcare-service-edit')

questionnaireAction(<Trans>Add patient</Trans>, 'patient-create', {
icon: <PlusOutlined />,
extra: {
modalProps: { width: 800 },
},
})

extra.qrfProps and extra.modalProps are web-specific options (WebExtra).

Navigates with React Router. The row resource is passed in location.state:

navigationAction(<Trans>Chart</Trans>, `/patients/${record.resource.id}`)

Use this to open a ResourceDetailPage route.

Custom actions

Render any React node in the Actions column or batch bar:

customAction(<MyCustomButton onDone={manager.reload} record={record} />)

Action scopes

ScopeCallbackTypical use
HeadergetHeaderActionsCreate, import
RowgetRecordActions(record, { reload })Edit, open detail, video call
BatchgetBatchActions(selectedBundle)Bulk update, bulk delete

Batch actions appear above the table when getBatchActions returns at least one action. Row checkboxes are enabled automatically.

Filters and sorting

Filters — return SearchBarColumn[] from getFilters(). Values are debounced (300 ms) and mapped to FHIR search params via each column's searchParam or id.

Sorters — return SorterColumn[] from getSorters():

const getSorters = () => [
{ id: 'date', searchParam: 'date', label: 'Date' },
];

Column header clicks update _sort. The component appends uniqueOrderSortSearchParam (default -_lastUpdated) as a tie-breaker so pagination stays stable.

Set initial sort in searchParams:

searchParams={{ _sort: '-date,_id', _count: 10 }}

Data loading

useResourceListPage calls usePager with resourceType and merged search params. Each page returns a bundle; primary resources are extracted and wrapped as { resource, bundle }.

Use _include / _include:iterate in searchParams to load related resources for column rendering:

searchParams={{
'_include:iterate': [
'Encounter:subject',
'Encounter:participant:PractitionerRole',
'Encounter:participant:Practitioner',
'PractitionerRole:practitioner:Practitioner',
],
_sort: '-date,_id',
_count: 10,
}}

Role-based scoping is a common pattern — filter searchParams by the current user before passing them to the list:

const roleSearchParams = matchCurrentUserRole({
[Role.Practitioner]: (practitioner) => ({ participant: practitioner.id }),
[Role.Admin]: () => ({}),
// ...
});

<ResourceListPage
searchParams={{ ...roleSearchParams, _sort: '-date,_id', _count: 10 }}
// ...
/>

Pairing with detail pages

List pages typically link to detail pages via navigationAction:

const getRecordActions = (record: RecordType<Encounter>) => [
navigationAction(<Trans>Open</Trans>, `/patients/${patientId}/encounters/${record.resource.id}`),
];

Detail pages can embed ResourceListPageContent inside a tab for related resources. See Resource detail page — Nested lists.

Examples

Encounter list — includes, custom format, navigation

EncounterList — standalone page with _include:iterate, custom column format functions, filters, sorters, and row navigation:

<ResourceListPage
headerTitle={t`Encounters`}
resourceType="Encounter"
searchParams={{
'_include:iterate': [
'Encounter:subject',
'Encounter:participant:PractitionerRole',
'Encounter:participant:Practitioner',
'PractitionerRole:practitioner:Practitioner',
],
_sort: '-date,_id',
_count: 10,
}}
getTableColumns={getTableColumns}
getRecordActions={(record) => [
navigationAction(<Trans>Open</Trans>, `/patients/${patientId}/encounters/${record.resource.id}`),
navigationAction(<Trans>Video call</Trans>, `/encounters/${record.resource.id}/video`),
]}
getFilters={getFilters}
getSorters={getSorters}
/>

Healthcare service list — FHIRPath columns and questionnaire actions

HealthcareServiceList — declarative FHIRPath getters with header and row questionnaire actions:

<ResourceListPage
headerTitle={t`Healthcare Services`}
resourceType="HealthcareService"
searchParams={{ _sort: '-_lastUpdated,_id', _count: 10 }}
getFilters={getFilters}
getSorters={getSorters}
getTableColumns={getTableColumns}
getRecordActions={getRecordActions}
getHeaderActions={getHeaderActions}
/>

PatientList — when the list searches Consent but questionnaires expect Patient, override getClinicalContext:

<ResourceListPage<Consent>
headerTitle={t`Patients`}
resourceType="Consent"
getClinicalContext={(record) => {
if (!record) {
return getResourceClinicalContext('Patient', {} as FhirResource);
}
const patient = getPatientFromConsent(record.resource, record.bundle);
return patient ? getResourceClinicalContext('Patient', patient) : [];
}}
// ...
/>

Nested list inside a detail tab

MedicationManagementDetailResourceListPageContent on the overview tab lists related Medication batches:

<ResourceListPageContent<Medication>
resourceType="Medication"
searchParams={{ code, status: 'active' }}
getHeaderActions={() => [questionnaireAction(t`Add batch`, 'medication-batch-create')]}
getTableColumns={() => [
{
title: t`Lot number`,
key: 'lotNumber',
render: (_: unknown, record) => getMedicationLotNumber(record.resource) ?? '',
},
]}
getClinicalContext={(record) => [
...getResourceClinicalContext('Medication', record?.resource ?? ({} as FhirResource)),
...getResourceClinicalContext('MedicationKnowledge', resource, ['CurrentMedicationKnowledge']),
]}
/>

PatientEncounter — encounters list embedded in a patient chart with defaultLaunchContext for the current patient:

<ResourceListPageContent<Encounter>
resourceType="Encounter"
searchParams={{ subject: patient.id, _sort: '-date,_id', _count: 10 }}
getTableColumns={getTableColumns}
getRecordActions={getRecordActions}
getHeaderActions={getHeaderActions}
defaultLaunchContext={[{ name: 'Patient', resource: patient }]}
/>

Clinical context

Questionnaire actions merge launch context from three sources: ancestor ClinicalContext, defaultLaunchContext, and getClinicalContext(record).

Action scopegetClinicalContext argumentDefault
Header (create)undefinedEmpty array
Row action{ resource, bundle }Primary resource (PascalCase + lowercase names)
Batch (multi-select)One call per selected rowParameters concatenated per row

On ResourceListPage, header questionnaire actions automatically merge getClinicalContext(undefined). On ResourceListPageContent, pass patient or parent resource context via defaultLaunchContext or getClinicalContext.

Full details and examples: Clinical context — Resource list pages.